from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from lib.lshape import Shape
import utils
import time
import functools
import os.path as osp
import os
import json

from lib.label_file import LabelFile
from lib.label_file import LabelFileError

from .labelDialog import LabelDialog
from lib.pconf import Config, sys_cfg

CURSOR_DEFAULT = Qt.ArrowCursor
CURSOR_POINT = Qt.PointingHandCursor
CURSOR_DRAW = Qt.CrossCursor
CURSOR_GRAB = Qt.OpenHandCursor
CURSOR_MOVE = Qt.ClosedHandCursor


MOVE_SPEED = 5.0

QT5 = True

_fill_drawing = False

class DrawBox(QWidget):
    # zoomRequest = pyqtSignal(int , QPoint)
    scrollRequest = pyqtSignal(int , int)
    newShape = pyqtSignal()
    selectionChanged = pyqtSignal(list)
    shapeMoved = pyqtSignal()
    drawingPolygon = pyqtSignal(bool)
    vertexSelected = pyqtSignal(bool)
    toggleDrawSignal = pyqtSignal(bool)
    
    zoomRequest = pyqtSignal(int)
    CREATE, EDIT = list(range(2))
    epsilon = 50.0
    base_epsilon = 50.0
    # epsilon = 50.0

    def __init__(self, rectVertex, update, parent):
        super(DrawBox, self).__init__()
        # self.update = update
        self.epsilon = 10.0
        self.double_click = "close"
        if self.double_click not in [None , "close"] :
            raise ValueError(
                "Unexpected value for double_click event: {}".format(
                    self.double_click
                )
            )
        self.num_backups = 10

        self.polyShape = Shape(shape_type="polygon")
        self._fill_drawing = False

        # Initialise local state.
        self.mode = self.EDIT
        self.shapes = []
        self.shapesBackups = []
        self.current = None
        self.selectedShapes = []  # save the selected shapes here
        self.selectedShapesCopy = []
        # self.line represents:
        #   - createMode == 'polygon': edge from last point to current
        #   - createMode == 'rectangle': diagonal line of the rectangle
        #   - createMode == 'line': the line
        #   - createMode == 'point': the point
        self.line = Shape(shape_type="polygon")
        self.prevPoint = QPoint()
        self.prevMovePoint = QPoint()
        self.offsets = QPoint() , QPoint()
        self.scale = 1.0
        self.pixmap = QPixmap()
        self.visible = {}
        self._hideBackround = False
        self.hideBackround = False
        self.hShape = None
        self.prevhShape = None
        self.hVertex = None
        self.prevhVertex = None
        self.hEdge = None
        self.prevhEdge = None
        self.movingShape = False
        self.snapping = True
        self.hShapeIsSelected = False
        self._painter = QPainter()
        self._cursor = CURSOR_DEFAULT
        # Menus:
        # 0: right-click without selection and dragging of shapes
        # 1: right-click with selection and dragging of shapes
        self.menus = (QMenu() , QMenu())
        # Set widget options.
        self.setMouseTracking(True)
        self.setFocusPolicy(Qt.WheelFocus)

        self.setupMenu()


    def contextMenuEvent(self, event):
        if not self.pixmap:
            return

        pos = self.transformPos(event.pos())
        self.mouseRightButtonPressPoint = pos
        self.menu.exec_(QCursor.pos())

    def setupMenu(self):
        self.menu = QMenu(self)
        createMode = QAction('&标注图片' , self)
        self.menu.addAction(createMode)
        createMode.triggered.connect(lambda : self.toggleDrawMode(False , createMode="polygon") )

        editMode = QAction('&编辑标注' , self)
        self.menu.addAction(editMode)
        editMode.triggered.connect(self.setEditMode)

        delete = QAction('&删除标注' , self)
        self.menu.addAction(delete)
        delete.triggered.connect(self.deleteSelectedShape)

        save = QAction('&保存标注', self)
        self.menu.addAction(save)
        save.triggered.connect(self.saveFile)



    def toggleDrawMode(self, edit=True, createMode="polygon"):
        self.setEditing(edit)
        self.createMode = createMode

    def setEditSignal(self):
        self.toggleDrawSignal.emit(True)

    def setEditMode(self):
        self.toggleDrawMode(True)

    def deleteSelectedShape(self):
        yes, no = QMessageBox.Yes, QMessageBox.No
        msg = self.tr(
            "You are about to permanently delete {} polygons, "
            "proceed anyway?"
        ).format(len(self.selectedShapes))
        if yes == QMessageBox.warning(
            self, self.tr("Attention"), msg, yes | no, yes
        ):
            self.shapes = []
            self.storeShapes()
            self.restoreShape()
            self.shapes = []
            # self.remLabels(self.deleteSelected())
            # self.setDirty()

    def setDirty(self):
        # Even if we autosave the file, we keep the ability to undo
        # self.actions.undo.setEnabled(self.canvas.isShapeRestorable)


        # if self._config["auto_save"] or self.actions.saveAuto.isChecked():
        # if :
        #     label_file = osp.splitext(self.imagePath)[0] + ".json"
        #     if self.output_dir:
        #         label_file_without_path = osp.basename(label_file)
        #         label_file = osp.join(self.output_dir, label_file_without_path)
        #     self.saveLabels(label_file)
        #     return
        self.dirty = True
        self.actions.save.setEnabled(True)
        __appname__ = "deepSystem"
        title = __appname__
        if self.filename is not None:
            title = "{} - {}*".format(title, self.filename)
        self.setWindowTitle(title)

    def saveFile(self, _value=False):
        self.saveLabels(sys_cfg["current_img_path"])


    def saveLabels(self, filename):
        lf = LabelFile()

        def format_shape(s):
            # PyQt4 cannot handle QVariant
            if isinstance(s, QVariant):
                s = s.toPyObject()

            print(s)
            # data = s.other_data.copy()
            data = dict()
            data.update(
                dict(
                    points=[(p.x(), p.y()) for p in s.points],
                )
            )
            return data

        self.storeShapes()
        self.restoreShape()
        shapes = [format_shape(item) for item in self.shapes]
        print("format_shapes:" , shapes)
        basename = os.path.basename(filename)
        basename = basename.rsplit('.' , 1)[0]


        data = dict(
            filename=filename,
            shapes=shapes,
        )

        with open(os.path.join(sys_cfg['project_path'], basename+".json") , "w") as f :
            json.dump(data , f , ensure_ascii=False , indent=2)

        # temp_cfg = Config()
        # temp_cfg.cfg_merge(sys_cfg)
        # temp_cfg["polygon_labels"] = data
        # temp_cfg.save(os.path.join(temp_cfg['project_path'], temp_cfg['project_name']+'.yaml'))


    def fillDrawing(self) :
        return self._fill_drawing

    def setFillDrawing(self , value) :
        self._fill_drawing = value

    @property
    def createMode(self) :
        return self._createMode

    @createMode.setter
    def createMode(self , value) :
        if value not in [
            "polygon" ,
            "rectangle" ,
            "circle" ,
            "line" ,
            "point" ,
            "linestrip" ,
        ] :
            raise ValueError("Unsupported createMode: %s" % value)
        self._createMode = value

    def storeShapes(self) :
        shapesBackup = []
        for shape in self.shapes :
            shapesBackup.append(shape.copy())
        if len(self.shapesBackups) > self.num_backups :
            self.shapesBackups = self.shapesBackups[-self.num_backups - 1 :]
        self.shapesBackups.append(shapesBackup)

    @property
    def isShapeRestorable(self) :
        # We save the state AFTER each edit (not before) so for an
        # edit to be undoable, we expect the CURRENT and the PREVIOUS state
        # to be in the undo stack.
        if len(self.shapesBackups) < 2 :
            return False
        return True

    def restoreShape(self) :
        # This does _part_ of the job of restoring shapes.
        # The complete process is also done in app.py::undoShapeEdit
        # and app.py::loadShapes and our own DrawBox::loadShapes function.
        if not self.isShapeRestorable :
            return
        self.shapesBackups.pop()  # latest

        # The application will eventually call DrawBox.loadShapes which will
        # push this right back onto the stack.
        shapesBackup = self.shapesBackups.pop()
        self.shapes = shapesBackup
        self.selectedShapes = []
        for shape in self.shapes :
            shape.selected = False
        self.update()

    def enterEvent(self , ev) :
        self.overrideCursor(self._cursor)

    def leaveEvent(self , ev) :
        self.unHighlight()
        self.restoreCursor()

    def focusOutEvent(self , ev) :
        self.restoreCursor()

    def isVisible(self , shape) :
        # print(shape)
        # print(type(shape))
        # print(self.visible)
        # print(type(self.visible))
        return self.visible.get(shape , True)

    def drawing(self) :
        return self.mode == self.CREATE

    def editing(self) :
        return self.mode == self.EDIT

    def setEditing(self , value=True) :
        self.mode = self.EDIT if value else self.CREATE
        if not value :  # Create
            self.unHighlight()
            self.deSelectShape()

    def unHighlight(self) :
        if self.hShape :
            self.hShape.highlightClear()
            self.update()
        self.prevhShape = self.hShape
        self.prevhVertex = self.hVertex
        self.prevhEdge = self.hEdge
        self.hShape = self.hVertex = self.hEdge = None

    def selectedVertex(self) :
        return self.hVertex is not None

    def selectedEdge(self) :
        return self.hEdge is not None

    def mouseMoveEvent(self , ev) :
        """Update line with last point and current coordinates."""
        try :
            if QT5 :
                pos = self.transformPos(ev.localPos())
            else :
                pos = self.transformPos(ev.posF())
        except AttributeError :
            return

        self.prevMovePoint = pos
        self.restoreCursor()

        # Polygon drawing.
        if self.drawing() :
            self.line.shape_type = self.createMode

            self.overrideCursor(CURSOR_DRAW)
            if not self.current :
                return

            if self.outOfPixmap(pos) :
                # Don't allow the user to draw outside the pixmap.
                # Project the point to the pixmap's edges.
                pos = self.intersectionPoint(self.current[-1] , pos)
            elif (
                    self.snapping
                    and len(self.current) > 1
                    and self.createMode == "polygon"
                    and self.closeEnough(pos , self.current[0])
            ) :
                # Attract line to starting point and
                # colorise to alert the user.
                pos = self.current[0]
                self.overrideCursor(CURSOR_POINT)
                self.current.highlightVertex(0 , Shape.NEAR_VERTEX)
            if self.createMode in ["polygon" , "linestrip"] :
                self.line[0] = self.current[-1]
                self.line[1] = pos
            elif self.createMode == "rectangle" :
                self.line.points = [self.current[0] , pos]
                self.line.close()
            elif self.createMode == "circle" :
                self.line.points = [self.current[0] , pos]
                self.line.shape_type = "circle"
            elif self.createMode == "line" :
                self.line.points = [self.current[0] , pos]
                self.line.close()
            elif self.createMode == "point" :
                self.line.points = [self.current[0]]
                self.line.close()
            self.repaint()
            self.current.highlightClear()
            return

        # Polygon copy moving.
        if Qt.RightButton & ev.buttons() :
            if self.selectedShapesCopy and self.prevPoint :
                self.overrideCursor(CURSOR_MOVE)
                self.boundedMoveShapes(self.selectedShapesCopy , pos)
                self.repaint()
            elif self.selectedShapes :
                self.selectedShapesCopy = [
                    s.copy() for s in self.selectedShapes
                ]
                self.repaint()
            return

        # Polygon/Vertex moving.
        if Qt.LeftButton & ev.buttons() :
            if self.selectedVertex() :
                self.boundedMoveVertex(pos)
                self.repaint()
                self.movingShape = True
            elif self.selectedShapes and self.prevPoint :
                self.overrideCursor(CURSOR_MOVE)
                self.boundedMoveShapes(self.selectedShapes , pos)
                self.repaint()
                self.movingShape = True
            return

        # Just hovering over the DrawBox, 2 possibilities:
        # - Highlight shapes
        # - Highlight vertex
        # Update shape/vertex fill and tooltip value accordingly.
        self.setToolTip(self.tr("Image"))
        for shape in reversed([s for s in self.shapes if self.isVisible(s)]) :
            # Look for a nearby vertex to highlight. If that fails,
            # check if we happen to be inside a shape.
            index = shape.nearestVertex(pos , self.epsilon / self.scale)
            index_edge = shape.nearestEdge(pos , self.epsilon / self.scale)
            if index is not None :
                if self.selectedVertex() :
                    self.hShape.highlightClear()
                self.prevhVertex = self.hVertex = index
                self.prevhShape = self.hShape = shape
                self.prevhEdge = self.hEdge
                self.hEdge = None
                shape.highlightVertex(index , shape.MOVE_VERTEX)
                self.overrideCursor(CURSOR_POINT)
                self.setToolTip(self.tr("Click & drag to move point"))
                self.setStatusTip(self.toolTip())
                self.update()
                break
            elif index_edge is not None and shape.canAddPoint() :
                if self.selectedVertex() :
                    self.hShape.highlightClear()
                self.prevhVertex = self.hVertex
                self.hVertex = None
                self.prevhShape = self.hShape = shape
                self.prevhEdge = self.hEdge = index_edge
                self.overrideCursor(CURSOR_POINT)
                self.setToolTip(self.tr("Click to create point"))
                self.setStatusTip(self.toolTip())
                self.update()
                break
            elif shape.containsPoint(pos) :
                if self.selectedVertex() :
                    self.hShape.highlightClear()
                self.prevhVertex = self.hVertex
                self.hVertex = None
                self.prevhShape = self.hShape = shape
                self.prevhEdge = self.hEdge
                self.hEdge = None
                self.setToolTip(
                    self.tr("Click & drag to move shape '%s'") % shape.label
                )
                self.setStatusTip(self.toolTip())
                self.overrideCursor(CURSOR_GRAB)
                self.update()
                break
        else :  # Nothing found, clear highlights, reset state.
            self.unHighlight()
        self.vertexSelected.emit(self.hVertex is not None)

    def addPointToEdge(self) :
        shape = self.prevhShape
        index = self.prevhEdge
        point = self.prevMovePoint
        if shape is None or index is None or point is None :
            return
        shape.insertPoint(index , point)
        shape.highlightVertex(index , shape.MOVE_VERTEX)
        self.hShape = shape
        self.hVertex = index
        self.hEdge = None
        self.movingShape = True

    def removeSelectedPoint(self) :
        shape = self.prevhShape
        index = self.prevhVertex
        if shape is None or index is None :
            return
        shape.removePoint(index)
        shape.highlightClear()
        self.hShape = shape
        self.prevhVertex = None
        self.movingShape = True  # Save changes

    def mousePressEvent(self , ev) :
        if QT5 :
            pos = self.transformPos(ev.localPos())
        else :
            pos = self.transformPos(ev.posF())
        if ev.button() == Qt.LeftButton :
            if self.drawing() :
                if self.current :
                    # Add point to existing shape.
                    if self.createMode == "polygon" :
                        self.current.addPoint(self.line[1])
                        self.line[0] = self.current[-1]
                        if self.current.isClosed() :
                            self.finalise()
                    elif self.createMode in ["rectangle" , "circle" , "line"] :
                        assert len(self.current.points) == 1
                        self.current.points = self.line.points
                        self.finalise()
                    elif self.createMode == "linestrip" :
                        self.current.addPoint(self.line[1])
                        self.line[0] = self.current[-1]
                        if int(ev.modifiers()) == Qt.ControlModifier :
                            self.finalise()
                elif not self.outOfPixmap(pos) :
                    # Create new shape.
                    self.current = Shape(shape_type=self.createMode)
                    self.current.addPoint(pos)
                    if self.createMode == "point" :
                        self.finalise()
                    else :
                        if self.createMode == "circle" :
                            self.current.shape_type = "circle"
                        self.line.points = [pos , pos]
                        self.setHiding()
                        self.drawingPolygon.emit(True)
                        self.update()
            elif self.editing() :
                if self.selectedEdge() :
                    self.addPointToEdge()
                elif (
                        self.selectedVertex()
                        and int(ev.modifiers()) == Qt.ShiftModifier
                ) :
                    # Delete point if: left-click + SHIFT on a point
                    self.removeSelectedPoint()

                group_mode = int(ev.modifiers()) == Qt.ControlModifier
                self.selectShapePoint(pos , multiple_selection_mode=group_mode)
                self.prevPoint = pos
                self.repaint()
        elif ev.button() == Qt.RightButton and self.editing() :
            group_mode = int(ev.modifiers()) == Qt.ControlModifier
            if not self.selectedShapes or (
                    self.hShape is not None
                    and self.hShape not in self.selectedShapes
            ) :
                self.selectShapePoint(pos , multiple_selection_mode=group_mode)
                self.repaint()
            self.prevPoint = pos

    def mouseReleaseEvent(self , ev) :
        if ev.button() == Qt.RightButton :
            menu = self.menus[len(self.selectedShapesCopy) > 0]
            self.restoreCursor()
            if (
                    not menu.exec_(self.mapToGlobal(ev.pos()))
                    and self.selectedShapesCopy
            ) :
                # Cancel the move by deleting the shadow copy.
                self.selectedShapesCopy = []
                self.repaint()
        elif ev.button() == Qt.LeftButton :
            if self.editing() :
                if (
                        self.hShape is not None
                        and self.hShapeIsSelected
                        and not self.movingShape
                ) :
                    self.selectionChanged.emit(
                        [x for x in self.selectedShapes if x != self.hShape]
                    )

        if self.movingShape and self.hShape :
            index = self.shapes.index(self.hShape)
            if (
                    self.shapesBackups[-1][index].points
                    != self.shapes[index].points
            ) :
                self.storeShapes()
                self.shapeMoved.emit()

            self.movingShape = False

    def endMove(self , copy) :
        assert self.selectedShapes and self.selectedShapesCopy
        assert len(self.selectedShapesCopy) == len(self.selectedShapes)
        if copy :
            for i , shape in enumerate(self.selectedShapesCopy) :
                self.shapes.append(shape)
                self.selectedShapes[i].selected = False
                self.selectedShapes[i] = shape
        else :
            for i , shape in enumerate(self.selectedShapesCopy) :
                self.selectedShapes[i].points = shape.points
        self.selectedShapesCopy = []
        self.repaint()
        self.storeShapes()
        return True

    def hideBackroundShapes(self , value) :
        self.hideBackround = value
        if self.selectedShapes :
            # Only hide other shapes if there is a current selection.
            # Otherwise the user will not be able to select a shape.
            self.setHiding(True)
            self.update()

    def setHiding(self , enable=True) :
        self._hideBackround = self.hideBackround if enable else False

    def canCloseShape(self) :
        return self.drawing() and self.current and len(self.current) > 2

    def mouseDoubleClickEvent(self , ev) :
        # We need at least 4 points here, since the mousePress handler
        # adds an extra one before this handler is called.
        if (
                self.double_click == "close"
                and self.canCloseShape()
                and len(self.current) > 3
        ) :
            self.current.popPoint()
            self.finalise()

    def selectShapes(self , shapes) :
        self.setHiding()
        self.selectionChanged.emit(shapes)
        self.update()

    def selectShapePoint(self , point , multiple_selection_mode) :
        """Select the first shape created which contains this point."""
        if self.selectedVertex() :  # A vertex is marked for selection.
            index , shape = self.hVertex , self.hShape
            shape.highlightVertex(index , shape.MOVE_VERTEX)
        else :
            for shape in reversed(self.shapes) :
                if self.isVisible(shape) and shape.containsPoint(point) :
                    self.setHiding()
                    if shape not in self.selectedShapes :
                        if multiple_selection_mode :
                            self.selectionChanged.emit(
                                self.selectedShapes + [shape]
                            )
                        else :
                            self.selectionChanged.emit([shape])
                        self.hShapeIsSelected = False
                    else :
                        self.hShapeIsSelected = True
                    self.calculateOffsets(point)
                    return
        self.deSelectShape()

    def calculateOffsets(self , point) :
        left = self.pixmap.width() - 1
        right = 0
        top = self.pixmap.height() - 1
        bottom = 0
        for s in self.selectedShapes :
            rect = s.boundingRect()
            if rect.left() < left :
                left = rect.left()
            if rect.right() > right :
                right = rect.right()
            if rect.top() < top :
                top = rect.top()
            if rect.bottom() > bottom :
                bottom = rect.bottom()

        x1 = left - point.x()
        y1 = top - point.y()
        x2 = right - point.x()
        y2 = bottom - point.y()
        self.offsets = QPoint(x1 , y1) , QPoint(x2 , y2)

    def boundedMoveVertex(self , pos) :
        index , shape = self.hVertex , self.hShape
        point = shape[index]
        if self.outOfPixmap(pos) :
            pos = self.intersectionPoint(point , pos)
        shape.moveVertexBy(index , pos - point)

    def boundedMoveShapes(self , shapes , pos) :
        if self.outOfPixmap(pos) :
            return False  # No need to move
        o1 = pos + self.offsets[0]
        if self.outOfPixmap(o1) :
            pos -= QPoint(min(0 , o1.x()) , min(0 , o1.y()))
        o2 = pos + self.offsets[1]
        if self.outOfPixmap(o2) :
            pos += QPoint(
                min(0 , self.pixmap.width() - o2.x()) ,
                min(0 , self.pixmap.height() - o2.y()) ,
            )
        # XXX: The next line tracks the new position of the cursor
        # relative to the shape, but also results in making it
        # a bit "shaky" when nearing the border and allows it to
        # go outside of the shape's area for some reason.
        # self.calculateOffsets(self.selectedShapes, pos)
        dp = pos - self.prevPoint
        if dp :
            for shape in shapes :
                shape.moveBy(dp)
            self.prevPoint = pos
            return True
        return False

    def deSelectShape(self) :
        if self.selectedShapes :
            self.setHiding(False)
            self.selectionChanged.emit([])
            self.hShapeIsSelected = False
            self.update()

    def deleteSelected(self) :
        deleted_shapes = []
        if self.selectedShapes :
            for shape in self.selectedShapes :
                self.shapes.remove(shape)
                deleted_shapes.append(shape)
            self.storeShapes()
            self.selectedShapes = []
            self.update()
        return deleted_shapes

    def deleteShape(self , shape) :
        if shape in self.selectedShapes :
            self.selectedShapes.remove(shape)
        if shape in self.shapes :
            self.shapes.remove(shape)
        self.storeShapes()
        self.update()

    def duplicateSelectedShapes(self) :
        if self.selectedShapes :
            self.selectedShapesCopy = [s.copy() for s in self.selectedShapes]
            self.boundedShiftShapes(self.selectedShapesCopy)
            self.endMove(copy=True)
        return self.selectedShapes

    def boundedShiftShapes(self , shapes) :
        # Try to move in one direction, and if it fails in another.
        # Give up if both fail.
        point = shapes[0][0]
        offset = QPoint(2.0 , 2.0)
        self.offsets = QPoint() , QPoint()
        self.prevPoint = point
        if not self.boundedMoveShapes(shapes , point - offset) :
            self.boundedMoveShapes(shapes , point + offset)

    def paintEvent(self , event) :
        if not self.pixmap :
            return super(DrawBox , self).paintEvent(event)

        p = self._painter
        p.begin(self)
        p.setRenderHint(QPainter.Antialiasing)
        p.setRenderHint(QPainter.HighQualityAntialiasing)
        p.setRenderHint(QPainter.SmoothPixmapTransform)

        p.scale(self.scale , self.scale)
        p.translate(self.offsetToCenter())

        p.drawPixmap(0 , 0 , self.pixmap)
        Shape.scale = self.scale
        for shape in self.shapes :
            print("type(shape):",type(shape))
            if (shape.selected or not self._hideBackround) and self.isVisible(
                    shape
            ) :
                shape.fill = shape.selected or shape == self.hShape
                shape.paint(p)
        if self.current :
            self.current.paint(p)
            self.line.paint(p)
        if self.selectedShapesCopy :
            for s in self.selectedShapesCopy :
                s.paint(p)

        if (
                self.fillDrawing()
                and self.createMode == "polygon"
                and self.current is not None
                and len(self.current.points) >= 2
        ) :
            drawing_shape = self.current.copy()
            drawing_shape.addPoint(self.line[1])
            drawing_shape.fill = True
            drawing_shape.paint(p)

        p.end()

    def transformPos(self , point) :
        """Convert from widget-logical coordinates to painter-logical ones."""
        return point / self.scale - self.offsetToCenter()

    def offsetToCenter(self) :
        s = self.scale
        area = super(DrawBox , self).size()
        w , h = self.pixmap.width() * s , self.pixmap.height() * s
        aw , ah = area.width() , area.height()
        x = (aw - w) / (2 * s) if aw > w else 0
        y = (ah - h) / (2 * s) if ah > h else 0
        return QPoint(x , y)

    def outOfPixmap(self , p) :
        w , h = self.pixmap.width() , self.pixmap.height()
        return not (0 <= p.x() <= w - 1 and 0 <= p.y() <= h - 1)

    def finalise(self) :
        assert self.current
        self.current.close()
        self.shapes.append(self.current)
        self.storeShapes()
        self.current = None
        self.setHiding(False)
        self.newShape.emit()
        self.update()

    def closeEnough(self , p1 , p2) :
        # d = distance(p1 - p2)
        # m = (p1-p2).manhattanLength()
        # print "d %.2f, m %d, %.2f" % (d, m, d - m)
        # divide by scale to allow more precision when zoomed in
        return utils.distance(p1 - p2) < (self.epsilon / self.scale)

    def intersectionPoint(self , p1 , p2) :
        # Cycle through each image edge in clockwise fashion,
        # and find the one intersecting the current line segment.
        # http://paulbourke.net/geometry/lineline2d/
        size = self.pixmap.size()
        points = [
            (0 , 0) ,
            (size.width() - 1 , 0) ,
            (size.width() - 1 , size.height() - 1) ,
            (0 , size.height() - 1) ,
        ]
        # x1, y1 should be in the pixmap, x2, y2 should be out of the pixmap
        x1 = min(max(p1.x() , 0) , size.width() - 1)
        y1 = min(max(p1.y() , 0) , size.height() - 1)
        x2 , y2 = p2.x() , p2.y()
        d , i , (x , y) = min(self.intersectingEdges((x1 , y1) , (x2 , y2) , points))
        x3 , y3 = points[i]
        x4 , y4 = points[(i + 1) % 4]
        if (x , y) == (x1 , y1) :
            # Handle cases where previous point is on one of the edges.
            if x3 == x4 :
                return QPoint(x3 , min(max(0 , y2) , max(y3 , y4)))
            else :  # y3 == y4
                return QPoint(min(max(0 , x2) , max(x3 , x4)) , y3)
        return QPoint(x , y)

    def intersectingEdges(self , point1 , point2 , points) :
        """Find intersecting edges.

        For each edge formed by `points', yield the intersection
        with the line segment `(x1,y1) - (x2,y2)`, if it exists.
        Also return the distance of `(x2,y2)' to the middle of the
        edge along with its index, so that the one closest can be chosen.
        """
        (x1 , y1) = point1
        (x2 , y2) = point2
        for i in range(4) :
            x3 , y3 = points[i]
            x4 , y4 = points[(i + 1) % 4]
            denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
            nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)
            nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)
            if denom == 0 :
                # This covers two cases:
                #   nua == nub == 0: Coincident
                #   otherwise: Parallel
                continue
            ua , ub = nua / denom , nub / denom
            if 0 <= ua <= 1 and 0 <= ub <= 1 :
                x = x1 + ua * (x2 - x1)
                y = y1 + ua * (y2 - y1)
                m = QPoint((x3 + x4) / 2 , (y3 + y4) / 2)
                d = utils.distance(m - QPoint(x2 , y2))
                yield d , i , (x , y)

    # These two, along with a call to adjustSize are required for the
    # scroll area.
    def sizeHint(self) :
        return self.minimumSizeHint()

    def minimumSizeHint(self) :
        if self.pixmap :
            return self.scale * self.pixmap.size()
        return super(DrawBox , self).minimumSizeHint()

    def wheelEvent(self, ev):

        delta = ev.angleDelta()
        v_delta = delta.y()

        self.zoomRequest.emit(v_delta)
        ev.accept()

    def moveByKeyboard(self , offset) :
        if self.selectedShapes :
            self.boundedMoveShapes(
                self.selectedShapes , self.prevPoint + offset
            )
            self.repaint()
            self.movingShape = True

    def keyPressEvent(self , ev) :
        modifiers = ev.modifiers()
        key = ev.key()
        if self.drawing() :
            if key == Qt.Key_Escape and self.current :
                self.current = None
                self.drawingPolygon.emit(False)
                self.update()
            elif key == Qt.Key_Return and self.canCloseShape() :
                self.finalise()
            elif modifiers == Qt.AltModifier :
                self.snapping = False
        elif self.editing() :
            if key == Qt.Key_Up :
                self.moveByKeyboard(QPoint(0.0 , -MOVE_SPEED))
            elif key == Qt.Key_Down :
                self.moveByKeyboard(QPoint(0.0 , MOVE_SPEED))
            elif key == Qt.Key_Left :
                self.moveByKeyboard(QPoint(-MOVE_SPEED , 0.0))
            elif key == Qt.Key_Right :
                self.moveByKeyboard(QPoint(MOVE_SPEED , 0.0))

    def keyReleaseEvent(self , ev) :
        modifiers = ev.modifiers()
        if self.drawing() :
            if int(modifiers) == 0 :
                self.snapping = True
        elif self.editing() :
            if self.movingShape and self.selectedShapes :
                index = self.shapes.index(self.selectedShapes[0])
                if (
                        self.shapesBackups[-1][index].points
                        != self.shapes[index].points
                ) :
                    self.storeShapes()
                    self.shapeMoved.emit()

                self.movingShape = False

    def setLastLabel(self , text , flags) :
        assert text
        self.shapes[-1].label = text
        self.shapes[-1].flags = flags
        self.shapesBackups.pop()
        self.storeShapes()
        return self.shapes[-1]

    def undoLastLine(self) :
        assert self.shapes
        self.current = self.shapes.pop()
        self.current.setOpen()
        if self.createMode in ["polygon" , "linestrip"] :
            self.line.points = [self.current[-1] , self.current[0]]
        elif self.createMode in ["rectangle" , "line" , "circle"] :
            self.current.points = self.current.points[0 :1]
        elif self.createMode == "point" :
            self.current = None
        self.drawingPolygon.emit(True)

    def undoLastPoint(self) :
        if not self.current or self.current.isClosed() :
            return
        self.current.popPoint()
        if len(self.current) > 0 :
            self.line[0] = self.current[-1]
        else :
            self.current = None
            self.drawingPolygon.emit(False)
        self.update()

    def loadPixmap(self , pixmap , clear_shapes=True) :
        self.pixmap = pixmap
        if clear_shapes :
            self.shapes = []
        self.update()

    def loadShapes(self , shapes , replace=True) :
        if replace :
            self.shapes = list(shapes)
        else :
            self.shapes.extend(shapes)
        self.storeShapes()
        self.current = None
        self.hShape = None
        self.hVertex = None
        self.hEdge = None
        self.update()

    def setShapeVisible(self , shape , value) :
        self.visible[shape] = value
        self.update()

    def overrideCursor(self , cursor) :
        self.restoreCursor()
        self._cursor = cursor
        QApplication.setOverrideCursor(cursor)

    def restoreCursor(self) :
        QApplication.restoreOverrideCursor()

    def resetState(self) :
        self.restoreCursor()
        self.pixmap = None
        self.shapesBackups = []
        self.update()